/**
 * ***************************************************************************** Copyright (c) 2000,
 * 2010 IBM Corporation and others. All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0 which accompanies this
 * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: IBM Corporation - initial API and implementation
 * *****************************************************************************
 */
package org.eclipse.ltk.core.refactoring;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.ltk.internal.core.refactoring.BufferValidationState;
import org.eclipse.ltk.internal.core.refactoring.Changes;
import org.eclipse.ltk.internal.core.refactoring.ContentStamps;
import org.eclipse.ltk.internal.core.refactoring.Lock;
import org.eclipse.ltk.internal.core.refactoring.RefactoringCorePlugin;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.UndoEdit;

/**
 * A change to perform the reverse change of a {@link TextFileChange}.
 *
 * <p>This class is not intended to be instantiated by clients. It is usually created by a <code>
 * TextFileChange</code> object.
 *
 * <p>The class should be subclassed by clients also subclassing <code>
 * TextFileChange</code> to provide a proper undo change object.
 *
 * @since 3.0
 */
public class UndoTextFileChange extends Change {

  private String fName;
  private UndoEdit fUndo;
  private IFile fFile;
  private ContentStamp fContentStampToRestore;
  private int fSaveMode;

  private boolean fDirty;
  private BufferValidationState fValidationState;

  /**
   * Create a new undo text file change object.
   *
   * @param name the human readable name of the change
   * @param file the file the change is working on
   * @param stamp the content stamp to restore when the undo is executed
   * @param undo the edit representing the undo modifications
   * @param saveMode the save mode as specified by {@link TextFileChange}
   * @see TextFileChange#KEEP_SAVE_STATE
   * @see TextFileChange#FORCE_SAVE
   * @see TextFileChange#LEAVE_DIRTY
   */
  protected UndoTextFileChange(
      String name, IFile file, UndoEdit undo, ContentStamp stamp, int saveMode) {
    Assert.isNotNull(name);
    Assert.isNotNull(file);
    Assert.isNotNull(undo);
    fName = name;
    fFile = file;
    fUndo = undo;
    fContentStampToRestore = stamp;
    fSaveMode = saveMode;
  }

  /**
   * Returns the change's save mode.
   *
   * @return the change's save mode
   * @see TextFileChange#KEEP_SAVE_STATE
   * @see TextFileChange#FORCE_SAVE
   * @see TextFileChange#LEAVE_DIRTY
   */
  public int getSaveMode() {
    return fSaveMode;
  }

  /** {@inheritDoc} */
  public String getName() {
    return fName;
  }

  /**
   * Hook to create an undo change for the given undo edit. This hook gets called while performing
   * the change to construct the corresponding undo change object.
   *
   * <p>Subclasses may override it to create a different undo change.
   *
   * @param edit the {@link UndoEdit undo edit} to create a undo change for
   * @param stampToRestore the content stamp to restore when the undo edit is executed.
   * @return the undo change
   * @throws CoreException if an undo change can't be created
   */
  protected Change createUndoChange(UndoEdit edit, ContentStamp stampToRestore)
      throws CoreException {
    return new UndoTextFileChange(getName(), fFile, edit, stampToRestore, fSaveMode);
  }

  /** {@inheritDoc} */
  public Object getModifiedElement() {
    return fFile;
  }

  /** {@inheritDoc} */
  public Object[] getAffectedObjects() {
    Object modifiedElement = getModifiedElement();
    if (modifiedElement == null) return null;
    return new Object[] {modifiedElement};
  }

  /** {@inheritDoc} */
  public void initializeValidationData(IProgressMonitor pm) {
    if (pm == null) pm = new NullProgressMonitor();
    pm.beginTask("", 1); // $NON-NLS-1$
    try {
      fValidationState = BufferValidationState.create(fFile);
    } finally {
      pm.done();
    }
  }

  /** {@inheritDoc} */
  public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException {
    if (pm == null) pm = new NullProgressMonitor();
    pm.beginTask("", 1); // $NON-NLS-1$
    try {
      if (fValidationState == null)
        throw new CoreException(
            new Status(
                IStatus.ERROR,
                RefactoringCorePlugin.getPluginId(),
                "UndoTextFileChange has not been initialialized")); // $NON-NLS-1$

      ITextFileBuffer buffer =
          FileBuffers.getTextFileBufferManager()
              .getTextFileBuffer(fFile.getFullPath(), LocationKind.IFILE);
      fDirty = buffer != null && buffer.isDirty();
      return fValidationState.isValid(needsSaving(), true);
    } finally {
      pm.done();
    }
  }

  /** {@inheritDoc} */
  public Change perform(IProgressMonitor pm) throws CoreException {
    if (pm == null) pm = new NullProgressMonitor();
    ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
    pm.beginTask("", 2); // $NON-NLS-1$
    ITextFileBuffer buffer = null;
    try {
      manager.connect(fFile.getFullPath(), LocationKind.IFILE, new SubProgressMonitor(pm, 1));
      buffer = manager.getTextFileBuffer(fFile.getFullPath(), LocationKind.IFILE);
      IDocument document = buffer.getDocument();
      ContentStamp currentStamp = ContentStamps.get(fFile, document);

      boolean[] setContentStampSuccess = {false};
      UndoEdit redo = performEdits(buffer, document, setContentStampSuccess);

      if (needsSaving()) {
        buffer.commit(pm, false);
        if (!setContentStampSuccess[0]) {
          // We weren't able to restore document stamp.
          // Since we save restore the file stamp instead
          ContentStamps.set(fFile, fContentStampToRestore);
        }
      }
      return createUndoChange(redo, currentStamp);
    } catch (BadLocationException e) {
      if (fValidationState == null || !fValidationState.wasDerived())
        throw Changes.asCoreException(e);
      else return new NullChange();
    } catch (MalformedTreeException e) {
      if (fValidationState == null || !fValidationState.wasDerived())
        throw Changes.asCoreException(e);
      else return new NullChange();
    } catch (CoreException e) {
      if (fValidationState == null || !fValidationState.wasDerived()) throw e;
      else return new NullChange();
    } finally {
      if (buffer != null)
        manager.disconnect(fFile.getFullPath(), LocationKind.IFILE, new SubProgressMonitor(pm, 1));
    }
  }

  private UndoEdit performEdits(
      ITextFileBuffer buffer, final IDocument document, final boolean[] setContentStampSuccess)
      throws MalformedTreeException, BadLocationException, CoreException {
    if (!buffer.isSynchronizationContextRequested()) {
      return doPerformEdits(document, setContentStampSuccess);
    }

    ITextFileBufferManager fileBufferManager = FileBuffers.getTextFileBufferManager();

    /** The lock for waiting for computation in the UI thread to complete. */
    final Lock completionLock = new Lock();
    final UndoEdit[] result = new UndoEdit[1];
    final BadLocationException[] badLocationException = new BadLocationException[1];
    final MalformedTreeException[] malformedTreeException = new MalformedTreeException[1];
    final CoreException[] coreException = new CoreException[1];
    Runnable runnable =
        new Runnable() {
          public void run() {
            synchronized (completionLock) {
              try {
                result[0] = doPerformEdits(document, setContentStampSuccess);
              } catch (BadLocationException e) {
                badLocationException[0] = e;
              } catch (MalformedTreeException e) {
                malformedTreeException[0] = e;
              } catch (CoreException e) {
                coreException[0] = e;
              } finally {
                completionLock.fDone = true;
                completionLock.notifyAll();
              }
            }
          }
        };

    synchronized (completionLock) {
      fileBufferManager.execute(runnable);
      while (!completionLock.fDone) {
        try {
          completionLock.wait(500);
        } catch (InterruptedException x) {
        }
      }
    }

    if (badLocationException[0] != null) {
      throw badLocationException[0];
    } else if (malformedTreeException[0] != null) {
      throw malformedTreeException[0];
    } else if (coreException[0] != null) {
      throw coreException[0];
    }
    return result[0];
  }

  private UndoEdit doPerformEdits(IDocument document, boolean[] setContentStampSuccess)
      throws MalformedTreeException, BadLocationException, CoreException {
    // perform the changes
    LinkedModeModel.closeAllModels(document);
    UndoEdit redo = fUndo.apply(document, TextEdit.CREATE_UNDO);

    // try to restore the document content stamp
    setContentStampSuccess[0] = ContentStamps.set(document, fContentStampToRestore);
    return redo;
  }

  /** {@inheritDoc} */
  public void dispose() {
    if (fValidationState != null) {
      fValidationState.dispose();
    }
  }

  private boolean needsSaving() {
    return (fSaveMode & TextFileChange.FORCE_SAVE) != 0
        || !fDirty && (fSaveMode & TextFileChange.KEEP_SAVE_STATE) != 0;
  }
}
